<?php
/**
 * This file is part of the Sphoof framework.
 * Copyright (c) 2010-2011 Sphoof
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code. You can also view the
 * LICENSE file online at http://www.sphoof.nl/new-bsd.txt
 *
 * @category    Sphoof
 * @copyright   Copyright (c) 2010-2011 Sphoof (http://sphoof.nl)
 * @license	    http://sphoof.nl/new-bsd.txt   New BSD License
 * @package     Container
 */

/**
 * Exception that will be thrown upon encountering an unknown scalar parameter.
 *
 * @package		Container
 * @subpackage	Exception
 */
class SpUnknownScalar extends SpException { }

/**
 * Exception that will be thrown upon encountering an unresolvable parameter.
 *
 * @package Container
 */
class SpUnknownDependency extends SpException { }

/**
 * Exception that will be thrown upon requesting a class that does not exist.
 *
 * @package Container
 */
class SpUnknownClass extends SpException { }

/**
 * Exception that will be thrown upon registering an injecting method that does not exist.
 *
 * @package Container
 */
class SpUnknownSetterMethod extends SpException { }

/**
 * Exception that will be thrown upon requesting an interface that has no
 * concrete implementation.
 *
 * @package Container
 */
class SpUnknownImplementation extends SpException { }

/**
 * An exception thrown when using incorrect arguments.
 *
 * @package Container
 */
class SpInvalidArgumentException extends SpException { }

/**
 * The dependency injection container.
 *
 * @package Container
 */
class SpContainer  {
	/**
	 * An optional context to look into.
	 *
	 * @var SpContainer
	 */
	protected $context;

	/**
	 * An array of instances
	 *
	 * @var array
	 */
	protected $instances;

	/**
	 * An array of scalar values.
	 *
	 * @var array
	 */
	protected $scalars;

	/**
	 * An array of implementations.
	 *
	 * @var array
	 */
	protected $implementations;

	/**
	 * An array of ReflectionClass instances.
	 *
	 * @var array
	 */
	protected $reflectedClasses;

	/**
	 * An array of ReflectionMethod instances.
	 *
	 * @var array
	 */
	protected $reflectedMethods;

	/**
	 * An array of class-specific overwrites.
	 *
	 * @var array
	 */
	protected $overwrites;

	/**
	 * An array of methods to inject into after instantation.
	 *
	 * @var array
	 */
	protected $injectMethods;

	/**
	 * Construct the container.
	 *
	 * @param SpContainer $context
	 */
	public function __construct( SpContainer $context = null ) {
		$this->context = $context;
		$this->registerInstance( 'SpContainer', $this );
	}

	/**
	 * Creates a new instance of class $classname
	 *
	 * @param string $classname
	 * @return object
	 */
	public function create( $classname ) {
		if( !$this->isAbstract( $concrete = $this->concrete( $classname ) ) ) {
			return $this->injectSetters(
				$this->build(
					$this->reflect( $concrete ),
					$this->resolve( $concrete, $this->parameters( $this->constructor( $concrete ) ) )
				)
			);
		}
	}


	/**
	 * Returns an object of the specified type or one of it's concrete implementations.
	 *
	 * @param String $abstract
	 * @return Object
	 * @throws SpUnknownDependency if a class or one of it's dependencies could not be found.
	 */
	public function get( $abstract, $requester = null ) {
		if( ( $abstract = strtolower( $abstract ) ) && $this->exists( $concrete = $this->concrete( $abstract, $requester ) ) ) {
			if( ( $object = $this->instance( $concrete ) ) ) {
				return $object;
			}
			throw new SpUnknownImplementation( 'Could not find implementation for ' . $abstract );
		}
		throw new SpUnknownClass( sprintf( 'Could not find the class %s', $abstract ) );
	}

	/**
	 * Creates a subcontainer which can be further configured and override specific values.
	 *
	 * @return SpContainer
	 */
	public function getSubcontainer( ) {
		return new SpContainer( $this );
	}


	/**
	 * Tell the container to call and inject $methods on classes of type $classname.
	 *
	 * @param string $classname
	 * @param string|array $methods
	 * @return SpContainer
	 */
	public function injectMethods( $classname, $methods ) {
		$this->injectMethods[$this->concrete( $classname )] = (array) $methods;
		return $this;
	}

	/**
	 * Registers an instance to use for class $abstract.
	 *
	 * @param string $abstract
	 * @param object $concrete
	 * @return SpContainer
	 */
	public function registerInstance( $abstract, $concrete ) {
		if( is_object( $concrete ) && is_string( $abstract ) ) {
			$this->instances[$this->concrete( $abstract )] = $concrete;
			return $this;
		}
		throw new SpInvalidArgumentException( );
	}

	/**
	 * Sets the scalar values a class that should be passed onto the class.
	 *
	 * @param String $classname
	 * @param Array $scalars
	 * @return SpContainer
	 */
	public function setScalars( $classname, Array $scalars ) {
		$this->scalars[strtolower( $classname )] = $scalars;
		return $this;
	}

	/**
	 * Specifies which concrete implementation of an abstract class, base class or interface should be used.
	 *
	 * @param String $abstract
	 * @param String $concrete
	 * @return SpContainer
	 */
	public function useImplementation( $abstract, $concrete, $forClass = null ) {
		if( null !== $forClass ) {
			$this->overwrites[strtolower( $forClass )][strtolower( $abstract )] = strtolower( $concrete );
			return $this;
		}
		$this->implementations[strtolower( $abstract )] = strtolower( $concrete );
		return $this;
	}

	/**
	 * Tries to instantiate the class $class with parameters $parameters.
	 *
	 * @param ReflectionClass $class
	 * @param array $parameters
	 * @return object
	 */
	protected function build( ReflectionClass $class, $parameters ) {
		return count( $parameters ) > 0 ? $class->newInstanceArgs( $parameters ) : $class->newInstance( );
	}

	/**
	 * Determines which concrete value should be injected into the class $requester.
	 *
	 * @param string $abstract
	 * @param string $requester
	 * @return string
	 */
	protected function concrete( $abstract, $requester = null ) {
		if( false === ( $overwrite = $this->overwrite( $abstract, $requester ) ) ) {
			if( ( $abstract = strtolower( $abstract ) ) && isset( $this->implementations[$abstract] ) ) {
				return $this->concrete( $this->implementations[$abstract] );
			}
			return strtolower( $abstract );
		}
		return $overwrite;
	}

	/**
	 * Gets and returns the constructor for the class $classname.
	 *
	 * @param string $classname
	 * @return ReflectionMethod
	 */
	protected function constructor( $classname ) {
		return $this->constructors[$classname] = $this->reflect( $classname )->getConstructor( );
	}

	/**
	 * Determines whether or not the class with the name $class exists.
	 *
	 * @param string $class
	 * @return boolean
	 */
	protected function exists( $class ) {
		return ( class_exists( $class ) || interface_exists( $class ) );
	}

	/**
	 * Tries to find a classname for the passed parameter $parameter.
	 *
	 * @param Reflectionparameter $parameter
	 * @return String
	 */
	protected function findClass( ReflectionParameter $parameter ) {
		try {
			return $parameter->getClass( );
		}
		catch( ReflectionException $e ) {
			throw new SpUnknownDependency( $e->getMessage( ) );
		}
	}

	/**
	 * Determines whether or not an instance of class $concrete was already made.
	 *
	 * @param string $concrete
	 * @return boolean
	 */
	protected function has( $concrete ) {
		return isset( $this->instances[$concrete] );
	}

	/**
	 * Determines wether or not there is a scalar value to inject.
	 *
	 * @param string $classname
	 * @param string $scalar
	 * @return boolean
	 */
	protected function hasScalar( $classname, $scalar ) {
		$classname = strtolower( $classname );
		return isset( $this->scalars[$classname][$scalar] ) ? $this->scalars[$classname][$scalar] : null;
	}

	/**
	 * Tries to find methods that start with "set", and tries to inject the
	 * correct values into those methods.
	 *
	 * @param Object $object
	 * @return Object
	 */
	protected function injectSetters( $object ) {
		if( ( $classname = $this->concrete( get_class( $object ) ) ) && isset( $this->injectMethods[$classname] ) ) {
			foreach( $this->injectMethods[$classname] as $method ) {
				if( !method_exists( $object, $method ) || !( $reflectedMethod = $this->method( $classname, $method ) ) ) {
					throw new SpUnknownSetterMethod( sprintf( 'Method %s does not exist in class %s', $method, $classname ) );
				}
				$reflectedMethod->invokeArgs( $object, $this->resolve( $classname, $this->parameters( $reflectedMethod ) ) );
			}
		}
		return $object;
	}

	/**
	 * Creates or retrieves an instance of type $abstract, to inject into $requester
	 *
	 * @param string $abstract
	 * @param string $requester
	 * @return object
	 */
	protected function instance( $abstract, $requester = null ) {
		if( ( $concrete = $this->concrete( $abstract, $requester ) ) && !$this->has( $concrete ) ) {
			if( ( $object = $this->create( $concrete ) ) ) {
				return $this->instances[strtolower( $abstract )] = $object;
			}
			return isset( $this->context ) ? $this->context->get( $abstract ) : false;
		}
		return $this->instances[$concrete];
	}

	/**
	 * Determines whether or not a class is either an abstract class or an interface.
	 *
	 * @param string $classname
	 * @return boolean
	 */
	protected function isAbstract( $classname ) {
		return $this->reflect( $classname )->isAbstract( ) || $this->reflect( $classname )->isInterface( );
	}

	/**
	 * Creates and caches a reflection method.
	 *
	 * @param string $classname
	 * @param string $method
	 * @return ReflectionMethod
	 */
	protected function method( $classname, $method ) {
		return $this->reflectedMethods[$classname][$method] = new ReflectionMethod( $classname, $method );
	}

	/**
	 * Retrieves a potentially overwritten value, or false if there is no
	 * overwrite.
	 *
	 * @param string $abstract
	 * @param string $requester
	 * @return string|false
	 */
	protected function overwrite( $abstract, $requester ) {
		$abstract = strtolower( $abstract );
		$requester = strtolower( $requester );
		return isset( $this->overwrites[$requester][$abstract] ) ? $this->overwrites[$requester][$abstract] : false;
	}

	/**
	 * Returns an array of ReflectionParameters, or an empty array if there are
	 * none.
	 *
	 * @param ReflectionMethod $method
	 * @return Array
	 */
	protected function parameters( $method ) {
		return ( $method instanceof ReflectionMethod ) ? $method->getParameters( ) : array( );
	}

	/**
	 * Returns a ReflectionClass for class $classname
	 *
	 * @param string $classname
	 * @return ReflectionClass
	 */
	protected function reflect( $classname ) {
		if( !isset( $this->reflectedClasses[$classname] ) ) {
			$this->reflectedClasses[$classname] = new ReflectionClass( $classname );
		}
		return $this->reflectedClasses[$classname];
	}

	/**
	 * Loops through the parameters of a given method and tries to resolve the
	 * values.
	 *
	 * @param string $classname
	 * @param array $parameters
	 * @return array
	 */
	protected function resolve( $classname, $parameters ) {
		foreach( $parameters as $parameter ) {
			if( $value = $this->value( $classname, $parameter ) ) {
				$values[] = $value;
			}
		}
		return isset( $values ) ? $values : array( );
	}

	/**
	 * Retrieves a scalar value for a class, returns null if it optional and not set.
	 *
	 * @param string $classname
	 * @param ReflectionParameter $parameter
	 * @return mixed
	 */
	protected function scalar( $classname, ReflectionParameter $parameter ) {
		if( ( $scalar = $this->hasScalar( $classname, $parameter->getName( ) ) ) || $parameter->isOptional( ) ) {
			return ( null !== $scalar ) ? $scalar : null;
		}
		throw new SpUnknownScalar( sprintf( 'Unknown scalar value "%s" on class "%s"', $parameter->getName( ), $classname ) );
	}

	/**
	 * Tries to find the value for parameter $parameter, which can be either an
	 * object or a scalar value.
	 *
	 * @param string $classname
	 * @param ReflectionParameter $parameter
	 * @return mixed
	 */
	protected function value( $classname, ReflectionParameter $parameter ) {
		if( $class = $this->findClass( $parameter ) ) {
			return $this->get( $class->getName( ), $classname );
		}
		return $this->scalar( $classname, $parameter );
	}
}
